Latviešu

Atklājiet patiesu daudzpavedienu darbību JavaScript. Šis visaptverošais ceļvedis aptver SharedArrayBuffer, Atomics, Web Workers un drošības prasības augstas veiktspējas tīmekļa lietojumprogrammām.

JavaScript SharedArrayBuffer: Dziļāka iedziļināšanās vienlaicīgajā programmēšanā tīmeklī

Gadu desmitiem JavaScript viena pavediena daba ir bijusi gan tās vienkāršības avots, gan būtisks veiktspējas šķērslis. Notikumu cikla modelis lieliski darbojas vairumam ar lietotāja saskarni saistītu uzdevumu, taču tas saskaras ar grūtībām, kad jāveic skaitļošanas ziņā intensīvas operācijas. Ilgstoši aprēķini var "iesaldēt" pārlūkprogrammu, radot nepatīkamu lietotāja pieredzi. Lai gan Web Workers piedāvāja daļēju risinājumu, ļaujot skriptiem darboties fonā, tiem bija savs būtisks ierobežojums: neefektīva datu komunikācija.

Ienāk SharedArrayBuffer (SAB) – jaudīga funkcija, kas fundamentāli maina spēles noteikumus, ieviešot patiesu, zema līmeņa atmiņas koplietošanu starp pavedieniem tīmeklī. Savienojumā ar Atomics objektu, SAB paver jaunu ēru augstas veiktspējas, vienlaicīgām lietojumprogrammām tieši pārlūkprogrammā. Tomēr ar lielu varu nāk liela atbildība un sarežģītība.

Šis ceļvedis jūs aizvedīs dziļākā ceļojumā vienlaicīgās programmēšanas pasaulē JavaScript valodā. Mēs izpētīsim, kāpēc mums tas ir nepieciešams, kā darbojas SharedArrayBuffer un Atomics, kritiskos drošības apsvērumus, kas jums jārisina, un praktiskus piemērus, lai jūs varētu sākt darbu.

Vecā pasaule: JavaScript viena pavediena modelis un tā ierobežojumi

Pirms mēs varam novērtēt risinājumu, mums pilnībā jāizprot problēma. JavaScript izpilde pārlūkprogrammā tradicionāli notiek vienā pavedienā, ko bieži sauc par "galveno pavedienu" vai "UI pavedienu".

Notikumu cikls

Galvenais pavediens ir atbildīgs par visu: jūsu JavaScript koda izpildi, lapas renderēšanu, atbildi uz lietotāja mijiedarbībām (piemēram, klikšķiem un ritināšanu) un CSS animāciju izpildi. Tas pārvalda šos uzdevumus, izmantojot notikumu ciklu, kas nepārtraukti apstrādā ziņojumu (uzdevumu) rindu. Ja uzdevuma pabeigšana aizņem ilgu laiku, tas bloķē visu rindu. Nekas cits nevar notikt – lietotāja saskarne sasalst, animācijas raustās, un lapa kļūst nereaģējoša.

Web Workers: solis pareizajā virzienā

Web Workers tika ieviesti, lai mazinātu šo problēmu. Web Worker būtībā ir skripts, kas darbojas atsevišķā fona pavedienā. Jūs varat pārcelt smagus aprēķinus uz "worker", atstājot galveno pavedienu brīvu, lai apstrādātu lietotāja saskarni.

Komunikācija starp galveno pavedienu un "worker" notiek, izmantojot postMessage() API. Kad jūs nosūtāt datus, tos apstrādā strukturētās klonēšanas algoritms. Tas nozīmē, ka dati tiek serializēti, kopēti un pēc tam deserializēti "worker" kontekstā. Lai gan šis process ir efektīvs, tam ir būtiski trūkumi lielu datu kopu gadījumā:

Iedomājieties video redaktoru pārlūkprogrammā. Visa video kadra (kas var būt vairāki megabaiti) sūtīšana uz "worker" un atpakaļ apstrādei 60 reizes sekundē būtu pārmērīgi dārga. Tieši šo problēmu SharedArrayBuffer tika izstrādāts, lai atrisinātu.

Spēles noteikumu mainītājs: Iepazīstinām ar SharedArrayBuffer

SharedArrayBuffer ir fiksēta garuma neapstrādātu bināro datu buferis, līdzīgs ArrayBuffer. Kritiskā atšķirība ir tā, ka SharedArrayBuffer var tikt koplietots starp vairākiem pavedieniem (piemēram, galveno pavedienu un vienu vai vairākiem Web Workers). Kad jūs "nosūtāt" SharedArrayBuffer, izmantojot postMessage(), jūs nesūtāt kopiju; jūs sūtāt atsauci uz to pašu atmiņas bloku.

Tas nozīmē, ka jebkuras izmaiņas, ko bufera datos veic viens pavediens, ir nekavējoties redzamas visiem pārējiem pavedieniem, kuriem ir atsauce uz to. Tas novērš dārgo kopēšanas un serializācijas soli, nodrošinot gandrīz tūlītēju datu koplietošanu.

Iedomājieties to šādi:

Koplietotās atmiņas bīstamība: sacensību apstākļi

Tūlītēja atmiņas koplietošana ir jaudīga, taču tā arī ievieš klasisku problēmu no vienlaicīgās programmēšanas pasaules: sacensību apstākļus (race conditions).

Sacensību apstākļi rodas, kad vairāki pavedieni mēģina vienlaicīgi piekļūt un modificēt tos pašus koplietotos datus, un galīgais rezultāts ir atkarīgs no neparedzamās secības, kādā tie tiek izpildīti. Apsveriet vienkāršu skaitītāju, kas glabājas SharedArrayBuffer. Gan galvenais pavediens, gan "worker" vēlas to palielināt.

  1. Pavediens A nolasa pašreizējo vērtību, kas ir 5.
  2. Pirms pavediens A var ierakstīt jauno vērtību, operētājsistēma to aptur un pārslēdzas uz pavedienu B.
  3. Pavediens B nolasa pašreizējo vērtību, kas joprojām ir 5.
  4. Pavediens B aprēķina jauno vērtību (6) un ieraksta to atpakaļ atmiņā.
  5. Sistēma pārslēdzas atpakaļ uz pavedienu A. Tas nezina, ka pavediens B kaut ko ir izdarījis. Tas turpina no vietas, kur apstājās, aprēķinot savu jauno vērtību (5 + 1 = 6) un ierakstot 6 atpakaļ atmiņā.

Lai gan skaitītājs tika palielināts divas reizes, gala vērtība ir 6, nevis 7. Operācijas nebija atomāras – tās bija pārtraucamas, kas noveda pie datu zuduma. Tieši tāpēc jūs nevarat izmantot SharedArrayBuffer bez tā svarīgākā partnera: Atomics objekta.

Koplietotās atmiņas sargs: Atomics objekts

Atomics objekts nodrošina statisku metožu kopu, lai veiktu atomāras operācijas ar SharedArrayBuffer objektiem. Atomāra operācija garantēti tiek veikta pilnībā, netiekot pārtraukta ar citām operācijām. Tā vai nu notiek pilnībā, vai nenotiek vispār.

Atomics izmantošana novērš sacensību apstākļus, nodrošinot, ka lasīšanas-modificēšanas-rakstīšanas operācijas ar koplietoto atmiņu tiek veiktas droši.

Galvenās Atomics metodes

Apskatīsim dažas no svarīgākajām metodēm, ko nodrošina Atomics.

Sinhronizācija: Vairāk nekā vienkāršas operācijas

Dažreiz ir nepieciešams vairāk nekā tikai droša lasīšana un rakstīšana. Ir nepieciešams, lai pavedieni koordinētu savu darbību un gaidītu viens uz otru. Izplatīts slikts paņēmiens ir "aktīvā gaidīšana" (busy-waiting), kur pavediens atrodas ciklā, nepārtraukti pārbaudot atmiņas vietu, vai nav notikušas izmaiņas. Tas tērē CPU ciklus un izlādē akumulatoru.

Atomics nodrošina daudz efektīvāku risinājumu ar wait() un notify().

Saliekam visu kopā: Praktisks ceļvedis

Tagad, kad mēs saprotam teoriju, apskatīsim soļus, kā ieviest risinājumu, izmantojot SharedArrayBuffer.

1. solis: Drošības priekšnoteikums – starpizcelsmes izolācija

Šis ir visbiežākais klupšanas akmens izstrādātājiem. Drošības apsvērumu dēļ SharedArrayBuffer ir pieejams tikai lapās, kas atrodas starpizcelsmes izolētā stāvoklī. Tas ir drošības pasākums, lai mazinātu spekulatīvās izpildes ievainojamības, piemēram, Spectre, kas potenciāli varētu izmantot augstas izšķirtspējas taimerus (ko nodrošina koplietotā atmiņa), lai nopludinātu datus starp dažādām izcelsmēm.

Lai iespējotu starpizcelsmes izolāciju, jums jākonfigurē savs tīmekļa serveris, lai tas galvenajam dokumentam nosūtītu divas specifiskas HTTP galvenes:


Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

To var būt sarežģīti iestatīt, īpaši, ja jūs paļaujaties uz trešo pušu skriptiem vai resursiem, kas nenodrošina nepieciešamās galvenes. Pēc servera konfigurēšanas jūs varat pārbaudīt, vai jūsu lapa ir izolēta, pārbaudot self.crossOriginIsolated īpašību pārlūkprogrammas konsolē. Tai jābūt true.

2. solis: Bufera izveide un koplietošana

Jūsu galvenajā skriptā jūs izveidojat SharedArrayBuffer un "skatu" uz to, izmantojot TypedArray, piemēram, Int32Array.

main.js:


// Vispirms pārbaudiet starpizcelsmes izolāciju!
if (!self.crossOriginIsolated) {
  console.error("Šī lapa nav starpizcelsmes izolēta. SharedArrayBuffer nebūs pieejams.");
} else {
  // Izveidojiet koplietojamu buferi vienam 32 bitu veselam skaitlim.
  const buffer = new SharedArrayBuffer(4);

  // Izveidojiet skatu uz buferi. Visas atomārās operācijas notiek uz skata.
  const int32Array = new Int32Array(buffer);

  // Inicializējiet vērtību indeksā 0.
  int32Array[0] = 0;

  // Izveidojiet jaunu "worker".
  const worker = new Worker('worker.js');

  // Nosūtiet KOPLIETOJAMO buferi uz "worker". Tā ir atsauces nodošana, nevis kopija.
  worker.postMessage({ buffer });

  // Klausieties ziņojumus no "worker".
  worker.onmessage = (event) => {
    console.log(`Worker paziņoja par pabeigšanu. Gala vērtība: ${Atomics.load(int32Array, 0)}`);
  };
}

3. solis: Atomāro operāciju veikšana "worker"

"Worker" saņem buferi un tagad var veikt atomāras operācijas ar to.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker saņēma koplietojamo buferi.");

  // Veiksim dažas atomāras operācijas.
  for (let i = 0; i < 1000000; i++) {
    // Droši palieliniet koplietoto vērtību.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker pabeidza palielināšanu.");

  // Signalizējiet galvenajam pavedienam, ka esam pabeiguši.
  self.postMessage({ done: true });
};

4. solis: Sarežģītāks piemērs – paralēla summēšana ar sinhronizāciju

Pievērsīsimies reālistiskākai problēmai: ļoti liela skaitļu masīva summēšanai, izmantojot vairākus "workers". Mēs izmantosim Atomics.wait() un Atomics.notify() efektīvai sinhronizācijai.

Mūsu koplietotajam buferim būs trīs daļas:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [statuss, pabeigušie_workers, rezultāts_zemais, rezultāts_augstais]
  // Mēs izmantojam divus 32 bitu veselos skaitļus rezultātam, lai izvairītos no pārpildes lielām summām.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 veseli skaitļi
  const sharedArray = new Int32Array(sharedBuffer);

  // Ģenerējiet dažus nejaušus datus apstrādei
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Izveidojiet nekoplietotu skatu "worker" datu daļai
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // Tas tiek kopēts
    });
  }

  console.log('Galvenais pavediens tagad gaida, kad "workers" pabeigs...');

  // Gaidiet, kamēr statusa karodziņš indeksā 0 kļūs par 1
  // Tas ir daudz labāk nekā while cikls!
  Atomics.wait(sharedArray, 0, 0); // Gaidīt, ja sharedArray[0] ir 0

  console.log('Galvenais pavediens pamodināts!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`Gala paralēlā summa ir: ${finalSum}`);

} else {
  console.error('Lapa nav starpizcelsmes izolēta.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Aprēķiniet summu šī "worker" datu daļai
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Atomāri pieskaitiet vietējo summu kopējai summai
  Atomics.add(sharedArray, 2, localSum);

  // Atomāri palieliniet 'pabeigušo workers' skaitītāju
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // Ja šis ir pēdējais "worker", kas pabeidz...
  const NUM_WORKERS = 4; // Reālā lietojumprogrammā būtu jānodod kā parametrs
  if (finishedCount === NUM_WORKERS) {
    console.log('Pēdējais worker pabeidza. Paziņo galvenajam pavedienam.');

    // 1. Iestatiet statusa karodziņu uz 1 (pabeigts)
    Atomics.store(sharedArray, 0, 1);

    // 2. Paziņojiet galvenajam pavedienam, kas gaida uz indeksu 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

Reālās pasaules lietošanas gadījumi un pielietojumi

Kur šī jaudīgā, bet sarežģītā tehnoloģija patiešām rada atšķirību? Tā izceļas lietojumprogrammās, kas prasa smagus, paralēli veicamus aprēķinus ar lielām datu kopām.

Izaicinājumi un noslēguma apsvērumi

Lai gan SharedArrayBuffer ir transformējošs, tas nav brīnumlīdzeklis. Tas ir zema līmeņa rīks, kas prasa rūpīgu apiešanos.

  1. Sarežģītība: Vienlaicīgā programmēšana ir bēdīgi slavena ar savu sarežģītību. Sacensību apstākļu un strupsceļu (deadlocks) atkļūdošana var būt neticami grūta. Jums ir jādomā citādi par to, kā tiek pārvaldīts jūsu lietojumprogrammas stāvoklis.
  2. Strupsceļi: Strupsceļš rodas, kad divi vai vairāki pavedieni tiek bloķēti uz visiem laikiem, katrs gaidot, kad otrs atbrīvos resursu. Tas var notikt, ja jūs nepareizi ieviešat sarežģītus bloķēšanas mehānismus.
  3. Drošības slogs: Starpizcelsmes izolācijas prasība ir būtisks šķērslis. Tā var salauzt integrācijas ar trešo pušu pakalpojumiem, reklāmām un maksājumu vārtejām, ja tās neatbalsta nepieciešamās CORS/CORP galvenes.
  4. Nav paredzēts katrai problēmai: Vienkāršiem fona uzdevumiem vai I/O operācijām tradicionālais Web Worker modelis ar postMessage() bieži ir vienkāršāks un pietiekams. Izmantojiet SharedArrayBuffer tikai tad, ja jums ir skaidrs, ar CPU saistīts šķērslis, kas ietver lielu datu apjomu.

Noslēgums

SharedArrayBuffer kopā ar Atomics un Web Workers pārstāv paradigmas maiņu tīmekļa izstrādē. Tas sagrauj viena pavediena modeļa robežas, aicinot pārlūkprogrammā jaunu klasi jaudīgu, veiktspējīgu un sarežģītu lietojumprogrammu. Tas nostāda tīmekļa platformu līdzvērtīgākā pozīcijā ar natīvo lietojumprogrammu izstrādi skaitļošanas ziņā intensīviem uzdevumiem.

Ceļojums vienlaicīgajā JavaScript ir izaicinošs, prasot stingru pieeju stāvokļa pārvaldībai, sinhronizācijai un drošībai. Bet izstrādātājiem, kas vēlas paplašināt tīmekļa iespēju robežas – no reāllaika audio sintēzes līdz sarežģītai 3D renderēšanai un zinātniskai skaitļošanai – SharedArrayBuffer apguve vairs nav tikai opcija; tā ir būtiska prasme nākamās paaudzes tīmekļa lietojumprogrammu veidošanai.